/****************************************************************************
 * sched/sched/sched_critmonitor.c
 *
 * SPDX-License-Identifier: Apache-2.0
 *
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.  The
 * ASF licenses this file to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the
 * License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
 * License for the specific language governing permissions and limitations
 * under the License.
 *
 ****************************************************************************/

/****************************************************************************
 * Included Files
 ****************************************************************************/

#include <nuttx/config.h>

#include <sys/types.h>
#include <sched.h>
#include <assert.h>
#include <debug.h>
#include <time.h>

#include "sched/sched.h"

/****************************************************************************
 * Pre-processor Definitions
 ****************************************************************************/

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_PREEMPTION > 0
#  define CHECK_PREEMPTION(pid, elapsed) \
     do \
       { \
         if (pid > 0 && \
             elapsed > CONFIG_SCHED_CRITMONITOR_MAXTIME_PREEMPTION) \
           { \
             CRITMONITOR_PANIC("PID %d hold sched lock too long %"PRIu32"\n", \
                               pid, elapsed); \
           } \
       } \
     while (0)
#else
#  define CHECK_PREEMPTION(pid, elapsed)
#endif

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_CSECTION > 0
#  define CHECK_CSECTION(pid, elapsed) \
     do \
       { \
         if (pid > 0 && \
             elapsed > CONFIG_SCHED_CRITMONITOR_MAXTIME_CSECTION) \
           { \
             CRITMONITOR_PANIC("PID %d hold critical section too long %" \
                               PRIu32 "\n", pid, elapsed); \
           } \
       } \
     while (0)
#else
#  define CHECK_CSECTION(pid, elapsed)
#endif

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_THREAD > 0
#  define CHECK_THREAD(pid, elapsed) \
     do \
       { \
         if (pid > 0 && \
             elapsed > CONFIG_SCHED_CRITMONITOR_MAXTIME_THREAD) \
           { \
             CRITMONITOR_PANIC("PID %d execute too long %"PRIu32"\n", \
                               pid, elapsed); \
           } \
       } \
     while (0)
#else
#  define CHECK_THREAD(pid, elapsed)
#endif

/****************************************************************************
 * Public Data
 ****************************************************************************/

/* Maximum time with pre-emption disabled or within critical section. */

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_PREEMPTION >= 0
clock_t g_preemp_max[CONFIG_SMP_NCPUS];
#endif

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_CSECTION >= 0
clock_t g_crit_max[CONFIG_SMP_NCPUS];
#endif

/****************************************************************************
 * Private Functions
 ****************************************************************************/

/****************************************************************************
 * Name: nxsched_critmon_cpuload
 *
 * Description:
 *   Update the running time of all running threads when switching threads
 *
 * Input Parameters:
 *   tcb   - The task that we are performing the load operations on.
 *   current - The current time
 *   tick - The ticks that we process in this cpuload.
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

#ifdef CONFIG_SCHED_CPULOAD_CRITMONITOR
static void nxsched_critmon_cpuload(FAR struct tcb_s *tcb, clock_t current,
                                    clock_t tick)
{
  int i;
  UNUSED(i);

  /* Update the cpuload of the thread ready to be suspended */

  nxsched_process_taskload_ticks(tcb, tick);

  /* Update the cpuload of threads running on other CPUs */

#  ifdef CONFIG_SMP
  for (i = 0; i < CONFIG_SMP_NCPUS; i++)
    {
      FAR struct tcb_s *rtcb = current_task(i);

      if (tcb->cpu == rtcb->cpu)
        {
          continue;
        }

      nxsched_process_taskload_ticks(rtcb, tick);

      /* Update start time, avoid repeated statistics when the next call */

      rtcb->run_start = current;
    }
#  endif
}
#endif

/****************************************************************************
 * Public Functions
 ****************************************************************************/

/****************************************************************************
 * Name: nxsched_critmon_preemption
 *
 * Description:
 *   Called when there is any change in pre-emptible state of a thread.
 *
 * Assumptions:
 *   - Called within a critical section.
 *   - Never called from an interrupt handler
 *   - Caller is the address of the function that is changing the pre-emption
 *
 ****************************************************************************/

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_PREEMPTION >= 0
void nxsched_critmon_preemption(FAR struct tcb_s *tcb, bool state,
                                FAR void *caller)
{
  clock_t current = perf_gettime();

  /* Are we enabling or disabling pre-emption */

  if (state)
    {
      /* Disabling.. Save the thread start time */

      tcb->preemp_start  = current;
      tcb->preemp_caller = caller;
    }
  else
    {
      /* Re-enabling.. Check for the max elapsed time */

      clock_t elapsed = current - tcb->preemp_start;
      int cpu         = this_cpu();

      if (elapsed > tcb->preemp_max)
        {
          tcb->preemp_max        = elapsed;
          tcb->preemp_max_caller = tcb->preemp_caller;
          CHECK_PREEMPTION(tcb->pid, elapsed);
        }

      /* Check for the global max elapsed time */

      if (elapsed > g_preemp_max[cpu])
        {
          g_preemp_max[cpu] = elapsed;
        }
    }
}
#endif /* CONFIG_SCHED_CRITMONITOR_MAXTIME_PREEMPTION >= 0 */

/****************************************************************************
 * Name: nxsched_critmon_csection
 *
 * Description:
 *   Called when a thread enters or leaves a critical section.
 *
 * Assumptions:
 *   - Called within a critical section.
 *   - Never called from an interrupt handler
 *   - Caller is the address of the function that is entering the critical
 *
 ****************************************************************************/

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_CSECTION >= 0
void nxsched_critmon_csection(FAR struct tcb_s *tcb, bool state,
                              FAR void *caller)
{
  clock_t current = perf_gettime();

  /* Are we entering or leaving the critical section? */

  if (state)
    {
      /* Entering... Save the start time. */

      tcb->crit_start  = current;
      tcb->crit_caller = caller;
    }
  else
    {
      /* Leaving .. Check for the max elapsed time */

      clock_t elapsed = current - tcb->crit_start;
      int cpu         = this_cpu();

      if (elapsed > tcb->crit_max)
        {
          tcb->crit_max        = elapsed;
          tcb->crit_max_caller = tcb->crit_caller;
          CHECK_CSECTION(tcb->pid, elapsed);
        }

      /* Check for the global max elapsed time */

      if (elapsed > g_crit_max[cpu])
        {
          g_crit_max[cpu] = elapsed;
        }
    }
}
#endif /* CONFIG_SCHED_CRITMONITOR_MAXTIME_CSECTION >= 0 */

/****************************************************************************
 * Name: nxsched_switch_critmon
 *
 * Description:
 *   Called when a thread is switched, update the critical monitor data.
 *
 * Input Parameters:
 *   from - The thread that is being switched out.
 *   to   - The thread that is being switched in.
 *
 * Returned Value:
 *   None
 *
 ****************************************************************************/

void nxsched_switch_critmon(FAR struct tcb_s *from, FAR struct tcb_s *to)
{
  clock_t current = perf_gettime();
  clock_t elapsed = current - from->run_start;

#ifdef CONFIG_SCHED_CPULOAD_CRITMONITOR
  clock_t tick = elapsed * CLOCKS_PER_SEC / perf_getfreq();
  nxsched_critmon_cpuload(from, current, tick);
#endif

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_THREAD >= 0
  from->run_time += elapsed;
  to->run_time = current;
  if (elapsed > from->run_max)
    {
      from->run_max = elapsed;
      CHECK_THREAD(from->pid, elapsed);
    }
#endif

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_PREEMPTION >= 0

  /* Did this task disable preemption? */

  if (nxsched_islocked_tcb(from))
    {
      int cpu = this_cpu();

      /* Possibly re-enabling.. Check for the max elapsed time */

      elapsed = current - from->preemp_start;
      if (elapsed > from->preemp_max)
        {
          from->preemp_max        = elapsed;
          from->preemp_max_caller = from->preemp_caller;
          CHECK_PREEMPTION(from->pid, elapsed);
        }

      /* Suspend percore preemptible statistic and if necessary will
       * re-open in nxsched_resume_critmon.
       */

      if (elapsed > g_preemp_max[cpu])
        {
          g_preemp_max[cpu] = elapsed;
        }
    }

  if (to->lockcount > 0)
    {
      to->premp_start = current;
    }
#endif /* CONFIG_SCHED_CRITMONITOR_MAXTIME_PREEMPTION */

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_CSECTION >= 0

  /* Is this task in a critical section? */

  if (from->irqcount > 0)
    {
      int cpu = this_cpu();

      /* Possibly leaving .. Check for the max elapsed time */

      elapsed = current - from->crit_start;
      if (elapsed > from->crit_max)
        {
          from->crit_max        = elapsed;
          from->crit_max_caller = from->crit_caller;
          CHECK_CSECTION(from->pid, elapsed);
        }

      /* Check for the global max elapsed time */

      if (elapsed > g_crit_max[cpu])
        {
          g_crit_max[cpu] = elapsed;
        }
    }

  if (to->irqcount > 0)
    {
      to->crit_start = current;
    }

#endif /* CONFIG_SCHED_CRITMONITOR_MAXTIME_CSECTION */

#if CONFIG_SCHED_CRITMONITOR_MAXTIME_PREEMPTION >= 0

  /* Did this task disable pre-emption? */

  if (to->lockcount > 0)
    {
      /* Yes.. Save the start time */

      to->premp_start = current;
    }
#endif /* CONFIG_SCHED_CRITMONITOR_MAXTIME_PREEMPTION */
}

void nxsched_update_critmon(FAR struct tcb_s *tcb)
{
  clock_t current = perf_gettime();
  clock_t elapsed = current - tcb->run_start;

  if (tcb->task_state != TSTATE_TASK_RUNNING)
    {
      return;
    }

#ifdef CONFIG_SCHED_CPULOAD_CRITMONITOR
  clock_t tick = elapsed * CLOCKS_PER_SEC / perf_getfreq();
  nxsched_process_taskload_ticks(tcb, tick);
#endif

  tcb->run_start = current;
  tcb->run_time += elapsed;
  if (elapsed > tcb->run_max)
    {
      tcb->run_max = elapsed;
      CHECK_THREAD(tcb->pid, elapsed);
    }
}
